<?php

/**
 * @category BRail
 * @package BRail_Security
 */

/**
 * Proxy implementing secure SOAP services against the security service
 *
 * @category BRail
 * @package BRail_Security
 */
class BRail_Security_Services_Proxy
{
    private $url;
    private $username;
    private $secret;

    const KEY_SIZE   = 32;
    const BLOCK_SIZE = 16;
    const IV_SIZE = 16;
    const securityLabel = 'WS-Security';

    public function __construct($url, $username, $secret)
    {
        $this->url      = $url;
        $this->username = $username;
        $this->secret   = $secret;
    }

    protected function sendMessage($soapAction, $data)
    {
        $response = @file_get_contents($this->url, false, stream_context_create(
            array('http' =>
                array(
                    'method'  => 'POST',
                    'header'  =>
                        "Content-type: text/xml; charset=utf-8\r\n" .
                        "SOAPAction: \"$soapAction\"\r\n" .
                        "Expect:",
                    'content' => $this->getSoap($soapAction, $data)
                )
            )
        ));

        if (false === $response) {
            throw new Exception('Cannot connect to the secure service');
        }

        if (false === strpos($http_response_header[0], ' 200 ')) {
            throw new Exception('Invalid reply from secure service');
        }

        return $this->treatResponse($response);
    }

    public function treatResponse($response)
    {
        $xml = new SimpleXMLElement($response);

        $body = $xml->children('http://schemas.xmlsoap.org/soap/envelope/')
                    ->Body;

        if ($body->Fault) {
            $temp = $body->Fault->children();
            throw new SoapFault((string) $temp->faultcode, (string) $temp->faultstring);
        }
        $usernameToken =
            $xml->children('http://schemas.xmlsoap.org/soap/envelope/')
                ->Header
                ->children('http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd')
                ->Security
                ->UsernameToken;

        return new SimpleXMLElement(
            $this->decrypt(
                $body->children('http://www.w3.org/2001/04/xmlenc#')
                        ->EncryptedData
                        ->CipherData
                        ->CipherValue,
                $this->getKey(
                    $usernameToken->Nonce,
                    $usernameToken
                        ->children('http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd')
                        ->Created
                )
            )
        );
    }

    private function decrypt($data, $key)
    {
        $data = base64_decode($data);
        $decrytedData = mcrypt_decrypt(
                            MCRYPT_RIJNDAEL_128,
                            $key,
                            substr($data, self::IV_SIZE),
                            MCRYPT_MODE_CBC,
                            substr($data, 0, self::IV_SIZE)
                        );
        $decrytedDataLength = strlen($decrytedData);
        return substr($decrytedData, 0, $decrytedDataLength - ord($decrytedData[$decrytedDataLength - 1]));
    }

    private function uuid()
    {
        return sprintf('%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
            mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff),
            mt_rand(0, 0x0fff) | 0x4000,
            mt_rand(0, 0x3fff) | 0x8000,
            mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff));
    }

    private function getSoap($soapAction, $data)
    {
        $time = time();
        $created = gmdate('Y-m-d\TH:i:s\Z', $time - 10);

        $nonce = $this->generateNonce();
        $encryptedDataId = $this->uuid();
        $securityTokenId = $this->uuid();

        return
        '<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" ' .
            'xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" ' .
            'xmlns:xsd="http://www.w3.org/2001/XMLSchema" ' .
            'xmlns:wsa="http://schemas.xmlsoap.org/ws/2004/08/addressing" ' .
            'xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd" ' .
            'xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">' .
            '<soap:Header>' .
                "<wsa:Action>$soapAction</wsa:Action>" .
                '<wsa:MessageID>urn:uuid:' . $this->uuid() . '</wsa:MessageID>' .
                '<wsa:ReplyTo>' .
                    '<wsa:Address>http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous</wsa:Address>' .
                '</wsa:ReplyTo>' .
                '<wsa:To>' . $this->url . '</wsa:To>' .
                '<wsse:Security soap:mustUnderstand="1">' .
                    '<wsu:Timestamp wsu:Id="Timestamp-' . $this->uuid() . '">' .
                        "<wsu:Created>$created</wsu:Created>" .
                        "<wsu:Expires>" . gmdate('Y-m-d\TH:i:s\Z', $time + 60) . "</wsu:Expires>" .
                    '</wsu:Timestamp>' .
                    '<wsse:UsernameToken ' .
                        'xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd" ' .
                        'wsu:Id="SecurityToken-' . $securityTokenId . '">' .
                        '<wsse:Username>'.$this->username.'</wsse:Username>' .
                        "<wsse:Nonce>$nonce</wsse:Nonce>" .
                        "<wsu:Created>$created</wsu:Created>" .
                    '</wsse:UsernameToken>' .
                    '<xenc:ReferenceList xmlns:xenc="http://www.w3.org/2001/04/xmlenc#">' .
                        '<xenc:DataReference URI="#Enc-' . $encryptedDataId . '" />' .
                    '</xenc:ReferenceList>' .
                '</wsse:Security>' .
            '</soap:Header>' .
            '<soap:Body>' .
                '<xenc:EncryptedData Id="Enc-' . $encryptedDataId . '" Type="http://www.w3.org/2001/04/xmlenc#Content" ' .
                    'xmlns:xenc="http://www.w3.org/2001/04/xmlenc#">' .
                    '<xenc:EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#aes256-cbc" />' .
                    '<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">' .
                        '<wsse:SecurityTokenReference>' .
                            '<wsse:Reference URI="#SecurityToken-' . $securityTokenId . '" ' .
                                'ValueType="http://docs.oasis-open.org/wss/2004/01/' .
                                           'oasis-200401-wss-username-token-profile-1.0#UsernameToken" />' .
                        '</wsse:SecurityTokenReference>' .
                        '</KeyInfo>' .
                    "<xenc:CipherData><xenc:CipherValue>" . $this->encrypt($data, $this->getKey($nonce, $created)) .
                                     "</xenc:CipherValue></xenc:CipherData>" .
                '</xenc:EncryptedData>' .
            '</soap:Body>' .
        '</soap:Envelope>';
    }

    private function encrypt($data, $key)
    {
        srand(microtime(true));
        $iv = mcrypt_create_iv(self::IV_SIZE, MCRYPT_RAND);
        $rest = self::BLOCK_SIZE - (strlen($data) % self::BLOCK_SIZE);

        return base64_encode(
            $iv .
            mcrypt_encrypt(
                MCRYPT_RIJNDAEL_128,
                $key,
                $data . str_repeat(chr($rest), $rest),
                MCRYPT_MODE_CBC,
                $iv
            )
        );
    }

    private function getKey($nonce, $created)
    {
        $secret = utf8_decode($this->secret);
        $aValue = $seed = self::securityLabel . base64_decode($nonce) . utf8_decode($created);
        $chunk  = '';
        $index  = 0;
        $buffer = '';

        for ($i = 0; $i < self::KEY_SIZE; $i++) {
            if ($index >= strlen($chunk)) {
                $aValue = hash_hmac('sha1', $aValue, $secret, true);
                $chunk  = hash_hmac('sha1', $aValue . $seed, $secret, true);
                $index  = 0;
            }

            $buffer .= $chunk[$index++];
        }

        return $buffer;
    }

    private function generateNonce()
    {
        return base64_encode(substr(md5(rand()), 0, 16));
    }

    public function validateAndRedeemTicket($ticketId)
    {
        $resultNode = $this->sendMessage(
            'http://www.b-rail.be/Security/SecurityInterfaces/ValidateAndRedeemTicket',
            '<ValidateAndRedeemTicket xmlns="http://www.b-rail.be/Security/SecurityInterfaces">' .
                '<encryptedTicketId>' . htmlspecialchars($ticketId) . '</encryptedTicketId>' .
            '</ValidateAndRedeemTicket>')
                           ->ValidateAndRedeemTicketResult;

        $userData = new stdClass();
        $userData->applicationId    = (string) $resultNode->ApplicationId;
        $userData->clientIpAddress  = (string) $resultNode->ClientIpAddress;
        $userData->functionId       = (string) $resultNode->FunctionId;
        $userData->key              = (string) $resultNode->Key;
        $userData->language         = (string) $resultNode->Language;
        $userData->link             = (string) $resultNode->Link;
        $userData->objectGroupId    = (string) $resultNode->ObjectGroupId;
        $userData->objectId         = (string) $resultNode->ObjectId;
        $userData->password         = (string) $resultNode->Password;
        $userData->userName         = (string) $resultNode->UserName;

        return $userData;
    }
}
